package com.whately.core.database.db;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import com.google.common.collect.Lists;
import com.whately.core.base.constant.DebugSwitcher;
import com.whately.core.base.utils.GsonUtils;
import com.whately.core.database.common.constant.WhatelyDbConstant;
import com.whately.core.database.vo.DatabaseTxPackageVO;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.beans.factory.config.YamlMapFactoryBean;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.io.FileNotFoundException;
import java.util.*;

/**
 * 多数据源配置类
 */
@Configuration
@Slf4j
@AutoConfigureOrder(-200)
public class DataSourceBeanFactory {
    /******************************************************************************
     * 常量
     *****************************************************************************/
    //配置文件的关键字
    private  final String CONFIG_YML_KEY_WDS_DATABASES ="dbs";
    private  final String CONFIG_YML_KEY_WDS_READONLY_DB ="readonlyDB";
    private  final String CONFIG_YML_KEY_WDS_MAPPER ="mapper";
    private  final String CONFIG_YML_KEY_TX_PACKAGE="tx-package"; //事务的包
    private  final String CONFIG_YML_KEY_TX_CLOSE="tx-close"; //事务开启与否


    //url-mysql
    private final String mysql_url_formatter="jdbc:mysql://{}/{}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&nullCatalogMeansCurrent=true";
    private final String mysql_driver_class="com.mysql.cj.jdbc.Driver";

    /******************************************************************************
     *
     *    数据源信息
     *
     *****************************************************************************/

    /**
     * 动态数据源: 通过AOP在不同数据源之间动态切换
     * 将数据库实例写入到targetDataSources属性中，并且使用defaultTargetDataSource属性设置默认数据源。
     * @Primary 注解用于标识默认使用的 DataSource Bean，并注入到SqlSessionFactory的dataSource属性中去。
     *
     * @return
     */
    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(DebugSwitcher debugSwitcher) throws Exception{
        log.info("load dynamicDataSource begin ,debugSwitcher {}", GsonUtils.toJsonString(debugSwitcher));

        //加载databases.yml配置信息
        Map<String, Object> databaseYmlConfigs = loadDatabaseConfigs(debugSwitcher);


        //解析
        //存储所有的数据库 ,包括只读库
        Map<Object, Object> databaseAll = new LinkedHashMap<>();
        initSchema(debugSwitcher,databaseYmlConfigs,databaseAll);

        //动态数据源
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        dynamicDataSource.setTargetDataSources(databaseAll);

        //默认库
        if(StringUtils.isBlank(DatabaseCfgConstant.DEFAULT_DATABASE_SCHEMA)||null==databaseAll.get(DatabaseCfgConstant.DEFAULT_DATABASE_SCHEMA)){
            log.error("need default datasource config info");
            throw new RuntimeException("error,need appoint default datasource");
        }
        dynamicDataSource.setDefaultTargetDataSource(databaseAll.get(DatabaseCfgConstant.DEFAULT_DATABASE_SCHEMA));

        return dynamicDataSource;
    }

    /**
     * 构建connect url
     */
    private String getDbConnectUrl(String driverClassName,String[] addrPortAndDbName){
        String url;
         //打印完整的sql语句
        /*if(mysql_log4jdbc_driver_class.equals(driverClassName)){
            url = MessageFormatter.arrayFormat(mysql_log4jdbc_url_formatter, addrPortAndDbName).getMessage();
        }else{
            url = MessageFormatter.arrayFormat(mysql_url_formatter, addrPortAndDbName).getMessage();
        }*/

        url = MessageFormatter.arrayFormat(mysql_url_formatter, addrPortAndDbName).getMessage();

        return url;
    }

    private void initSchema(DebugSwitcher debugSwitcher,Map<String, Object> databaseYmlConfigs,Map<Object, Object> databaseAll){
        Map<String, Object> databaseListMap =(Map<String, Object>)databaseYmlConfigs.get(CONFIG_YML_KEY_WDS_DATABASES);
        for (Map.Entry<String, Object> dbInfo : databaseListMap.entrySet()) {
            String schema = StringUtils.trim(dbInfo.getKey());
            if(StringUtils.isBlank(schema)){
                log.error("schema is null or schema not allow! "+schema);
                throw new RuntimeException("schema is null");
            }

            if(databaseAll.keySet().contains(schema)){
                log.error("schema {} is duplicate ",schema);
                throw new RuntimeException("schema is duplicate");
            }

            Map<String, Object> master_dbinfo = (Map<String, Object>) dbInfo.getValue();

            String username =StringUtils.EMPTY;
            if(master_dbinfo.get("username")!=null){
                username = ""+master_dbinfo.get("username");
            }
            String password =StringUtils.EMPTY;
            if(master_dbinfo.get("password")!=null){
                password = ""+master_dbinfo.get("password");
            }
            Boolean isDefault =(Boolean)master_dbinfo.get("isDefault");
            String url =(String)master_dbinfo.get("url");

            String driverClassName =(String)master_dbinfo.get("driver-class-name");
            if(StringUtils.isBlank(driverClassName)){
                driverClassName = mysql_driver_class;
            }

            if(StringUtils.isBlank(url)){
                String addrPort =(String)master_dbinfo.get("addr-port");
                String database =StringUtils.EMPTY;
                if(master_dbinfo.get("database")!=null){
                    database = ""+master_dbinfo.get("database");
                }
                String[] addrPortAndDbName = new String[]{addrPort,database};
                url = getDbConnectUrl(driverClassName,addrPortAndDbName);
            }


            DataSourceProperties masterProperties= new DataSourceProperties();
            masterProperties.setDriverClassName(driverClassName);
            masterProperties.setUsername(username);
            masterProperties.setPassword(password);
            masterProperties.setUrl(url);


            addDatabaseGroupBySchema(debugSwitcher,databaseAll,schema,masterProperties,true);


            //从库，readOnly,可以是多个
            if(master_dbinfo.containsKey(CONFIG_YML_KEY_WDS_READONLY_DB)){
                List<String> readOnlyUrls =(List<String>)master_dbinfo.get(CONFIG_YML_KEY_WDS_READONLY_DB);
                for(int i=0;i<readOnlyUrls.size();i++){

                    String readOnlyUrl=readOnlyUrls.get(i);
                    if(!readOnlyUrl.startsWith("jdbc:")){
                        readOnlyUrl = getDbConnectUrl(driverClassName,readOnlyUrl.split("/"));
                    }
                    DataSourceProperties readOnlyProperties= new DataSourceProperties();
                    readOnlyProperties.setDriverClassName(driverClassName);
                    readOnlyProperties.setUsername(username);
                    readOnlyProperties.setPassword(password);
                    readOnlyProperties.setUrl(readOnlyUrl);

                    addDatabaseGroupBySchema(debugSwitcher,databaseAll,schema,readOnlyProperties,false);
                }
            }

            //是否默认库
            if(isDefault!=null&&isDefault){
                // 默认数据源
                if(StringUtils.isNotBlank(DatabaseCfgConstant.DEFAULT_DATABASE_SCHEMA)){
                    log.error("only one default db can be set !! already set {},now set {}", DatabaseCfgConstant.DEFAULT_DATABASE_SCHEMA,schema);
                    throw new RuntimeException("only one default db can be set in env");
                }
                // 默认数据源 value
                DatabaseCfgConstant.DEFAULT_DATABASE_SCHEMA = schema;
                log.info("              ****** find default schema <{}> ******",schema);
            }else{
                // 非默认数据源 ,才有必要加载mapper文件 和 事务设置
                //1. mapper ，如果是序列字典库作为非默认
                if(schema.equals(WhatelyDbConstant.DATABASE_COMMON_SEQ_DIC)){
                    for(String mapper: WhatelyDbConstant.DATABASE_COMMON_SEQ_DIC_MAPPERS){
                        DatabaseCfgConstant.MAPPERS_SCHEMA_INFO.put(mapper,schema);
                    }
                }else if (master_dbinfo.containsKey(CONFIG_YML_KEY_WDS_MAPPER)){
                    List<String> mapperList =(List<String>)master_dbinfo.get(CONFIG_YML_KEY_WDS_MAPPER);
                    List<String> checkSeqDic = Arrays.asList(WhatelyDbConstant.DATABASE_COMMON_SEQ_DIC_MAPPERS);
                    for(String mapper:mapperList){
                        if(checkSeqDic.contains(mapper)){
                            throw new RuntimeException("common database such as dic or sequence config error :"+mapper);
                        }
                        DatabaseCfgConstant.MAPPERS_SCHEMA_INFO.put(mapper,schema);
                    }
                }

                //2.事务设置
                if(master_dbinfo.containsKey(CONFIG_YML_KEY_TX_PACKAGE)){
                    List<String> txPackageList =(List<String>)master_dbinfo.get(CONFIG_YML_KEY_TX_PACKAGE);
                    if(CollUtil.isEmpty(txPackageList)){
                        throw new RuntimeException("txPackage error is blank ");
                    }
                   /* if(txPackage.contains("*")){
                        throw new RuntimeException("txPackage error :"+txPackage);
                    }*/
                    String txClose = master_dbinfo.containsKey(CONFIG_YML_KEY_TX_CLOSE)?(String)master_dbinfo.get(CONFIG_YML_KEY_TX_CLOSE):"";

                    DatabaseTxPackageVO bean = new DatabaseTxPackageVO();
                    bean.setSchema(schema);
                    bean.setTxClose(StringUtils.equals("close",txClose)?true:false);
                    for(String pk:txPackageList){
                        if(StringUtils.isNotBlank(pk)){
                            DatabaseCfgConstant.TX_POINT_MAP.put(pk,bean);
                        }
                    }

                }
            }
        }
        //end of for  !
    }

    /**
     * 1.放到所有的datasource中,返回给动态数据源使用 databaseAll
     * 2.按分组放到map中，后续拦截用 database_group_by_schema
     */
    private void addDatabaseGroupBySchema(DebugSwitcher debugSwitcher,Map<Object, Object> databaseAll,String schema,DataSourceProperties dataSourceProperties,boolean isMasterFlag){
        DataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();

        List<DataSource> ls = DatabaseCfgConstant.DATABASE_GROUP_BY_SCHEMA.get(schema);
        if(ls==null){
            ls = Lists.newArrayList();
            DatabaseCfgConstant.DATABASE_GROUP_BY_SCHEMA.put(schema,ls);
        }

        String key = schema;
        //如果是从库
        if(!isMasterFlag){
            key = schema +"-readonly-"+ ls.size();
        }

        //0.重设pool-info
        String driveClass = dataSourceProperties.getDriverClassName();
        resetDataSourcePoolInfo(debugSwitcher,dataSource,"datasource-pool-"+key,driveClass);

        //1.放到所有的datasource中,返回给动态数据源使用
        databaseAll.put(key, dataSource);

        //2.按分组放到map中，后续拦截用
        if(isMasterFlag){
            ls.add(0,dataSource);
        }else {
            ls.add(dataSource);
        }

        log.info("load database {} ",key);
        log.info("              master flag {} ,url {} ",isMasterFlag,dataSourceProperties.getUrl());

    }


    private void resetDataSourcePoolInfo(DebugSwitcher debugSwitcher,DataSource ds,String poolName,String driveClass) {
        Properties poolProperties = loadPoolConfigInfo(debugSwitcher);
        PropertiesPropertySource dsPoolPropertySources = new PropertiesPropertySource("ttt",poolProperties);
        try {
            //去掉前缀
            HikariDataSource target = (HikariDataSource)ds;

            Iterable<ConfigurationPropertySource> configurationPropertySourceIt=ConfigurationPropertySources.from(dsPoolPropertySources);
            Binder binder = new Binder(configurationPropertySourceIt);
            HikariDataSource commonPropertiesHikariDataSource = binder.bind("spring.datasource.hikari",HikariDataSource.class).get();
            BeanUtil.copyProperties(commonPropertiesHikariDataSource,target,CopyOptions.create().setIgnoreNullValue(true));

            //PropertySourcesBinder binder = new PropertySourcesBinder(propertySources);
            //binder.bindTo("spring.datasource.hikari",target);

            //由于这两个属性的名字与getter、setter不一致，导致copy不成功，需要set下
            target.setMinimumIdle(commonPropertiesHikariDataSource.getMinimumIdle());
            target.setMaximumPoolSize(commonPropertiesHikariDataSource.getMaximumPoolSize());

            if(StringUtils.isNotBlank(target.getConnectionTestQuery())&&StringUtils.isNotBlank(driveClass)){
                if(driveClass.contains("oracle")){
                    target.setConnectionTestQuery("SELECT 1  FROM dual");
                }else if(driveClass.contains("mysql")){
                    target.setConnectionTestQuery("SELECT 1");
                }
            }
            //放到最后,重新设置name
            target.setPoolName(poolName);
        } catch (Exception e) {
            throw new RuntimeException("load env info from configcenterenv.properties at classpath failed.");
        }
    }

    private Properties loadPoolConfigInfo(DebugSwitcher debugSwitcher) {
        try {
            String path = "profiles/"+debugSwitcher.getProfilesActive()+"/datasource-pool.properties";

            Resource resource = new ClassPathResource(path);
            if (!resource.exists()) {
                throw new FileNotFoundException("file not find "+path);
            }

            Properties  poolInfo = PropertiesLoaderUtils.loadProperties(resource);
            return poolInfo;
        } catch (Exception e) {
            throw new RuntimeException("load env info from configcenterenv.properties at classpath failed.");
        }
    }

    private Map<String, Object> loadDatabaseConfigs(DebugSwitcher debugSwitcher) throws Exception{
        log.info("load database yml from profiles active {}",debugSwitcher.getProfilesActive());
        YamlMapFactoryBean yaml = new YamlMapFactoryBean();
        String path = "profiles/"+debugSwitcher.getProfilesActive()+"/databases.yml";
        Resource resource = new ClassPathResource(path);
        if (!resource.exists()) {
            throw new FileNotFoundException("databases.yml file not find "+path);
        }
        yaml.setResources(resource);
        return yaml.getObject();
    }

    /*****************************************************************************
     * 配置@Transactional注解事物
     * 使用dynamicDataSource作为transactionManager的入参来构造DataSourceTransactionManager
     *
     * @return
     ****************************************************************************/
    @Bean
    public PlatformTransactionManager platformTransactionManager(DataSource dynamicDataSource) {
        PlatformTransactionManager platformTransactionManager = new DataSourceTransactionManager(dynamicDataSource);
        return platformTransactionManager;
    }

}
